package client

import (
	"strings"
	"time"

	"github.com/sahib/brig/server/capnp"
	h "github.com/sahib/brig/util/hashlib"
)

// MakeCommit creates a new commit from the current staging area.
// The commit will have the message `msg`.
func (ctl *Client) MakeCommit(msg string) error {
	call := ctl.api.Commit(ctl.ctx, func(p capnp.VCS_commit_Params) error {
		return p.SetMsg(msg)
	})

	_, err := call.Struct()
	return err
}

// Commit describes a single commit in more detail.
type Commit struct {
	Hash h.Hash
	Msg  string
	Tags []string
	Date time.Time
}

func convertCapCommit(capEntry *capnp.Commit) (*Commit, error) {
	result := Commit{}
	modTimeStr, err := capEntry.Date()
	if err != nil {
		return nil, err
	}

	if err := result.Date.UnmarshalText([]byte(modTimeStr)); err != nil {
		return nil, err
	}

	result.Hash, err = capEntry.Hash()
	if err != nil {
		return nil, err
	}

	result.Msg, err = capEntry.Msg()
	if err != nil {
		return nil, err
	}

	tagList, err := capEntry.Tags()
	if err != nil {
		return nil, err
	}

	tags := []string{}
	for idx := 0; idx < tagList.Len(); idx++ {
		tag, err := tagList.At(idx)
		if err != nil {
			return nil, err
		}

		tags = append(tags, tag)
	}

	result.Tags = tags
	return &result, nil
}

// Log lists all commits, starting with the newest one.
func (ctl *Client) Log() ([]Commit, error) {
	call := ctl.api.Log(ctl.ctx, func(p capnp.VCS_log_Params) error {
		return nil
	})

	results := []Commit{}
	result, err := call.Struct()
	if err != nil {
		return nil, err
	}

	entries, err := result.Entries()
	if err != nil {
		return nil, err
	}

	for idx := 0; idx < entries.Len(); idx++ {
		capEntry := entries.At(idx)
		result, err := convertCapCommit(&capEntry)
		if err != nil {
			return nil, err
		}

		results = append(results, *result)
	}

	return results, nil
}

// Tag tags a commit (`rev`) with a certain `name`.
func (ctl *Client) Tag(rev, name string) error {
	call := ctl.api.Tag(ctl.ctx, func(p capnp.VCS_tag_Params) error {
		if err := p.SetTagName(name); err != nil {
			return err
		}

		return p.SetRev(rev)
	})

	_, err := call.Struct()
	return err
}

// Untag removes the `name` tag.
func (ctl *Client) Untag(name string) error {
	call := ctl.api.Untag(ctl.ctx, func(p capnp.VCS_untag_Params) error {
		return p.SetTagName(name)
	})

	_, err := call.Struct()
	return err
}

// Reset restores the content of `path` to the state at `rev`.
// If `force` is true, it will overwrite the staging area if it needs to.
func (ctl *Client) Reset(path, rev string, force bool) error {
	call := ctl.api.Reset(ctl.ctx, func(p capnp.VCS_reset_Params) error {
		if err := p.SetPath(path); err != nil {
			return err
		}

		p.SetForce(force)
		return p.SetRev(rev)
	})

	_, err := call.Struct()
	return err
}

// Change describes a change of a node between two commits.
type Change struct {
	Path string
	Mask []string
	Head *Commit
	Next *Commit

	MovedTo         string
	WasPreviouslyAt string

	IsPinned   bool
	IsExplicit bool
}

// History returns a detailed set of changes that happened to the node at `path`.
func (ctl *Client) History(path string) ([]*Change, error) {
	call := ctl.api.History(ctl.ctx, func(p capnp.VCS_history_Params) error {
		return p.SetPath(path)
	})

	result, err := call.Struct()
	if err != nil {
		return nil, err
	}

	histList, err := result.History()
	if err != nil {
		return nil, err
	}

	results := []*Change{}
	for idx := 0; idx < histList.Len(); idx++ {
		entry := histList.At(idx)
		path, err := entry.Path()
		if err != nil {
			return nil, err
		}

		change, err := entry.Change()
		if err != nil {
			return nil, err
		}

		capHeadCmt, err := entry.Head()
		if err != nil {
			return nil, err
		}

		head, err := convertCapCommit(&capHeadCmt)
		if err != nil {
			return nil, err
		}

		movedTo, err := entry.MovedTo()
		if err != nil {
			return nil, err
		}

		wasPreviouslyAt, err := entry.WasPreviouslyAt()
		if err != nil {
			return nil, err
		}

		var next *Commit
		if entry.HasNext() {
			capNextCmt, err := entry.Next()
			if err != nil {
				return nil, err
			}

			next, err = convertCapCommit(&capNextCmt)
			if err != nil {
				return nil, err
			}
		}

		results = append(results, &Change{
			Path:            path,
			Mask:            strings.Split(change, "|"),
			Head:            head,
			Next:            next,
			MovedTo:         movedTo,
			WasPreviouslyAt: wasPreviouslyAt,
			IsPinned:        entry.IsPinned(),
			IsExplicit:      entry.IsExplicit(),
		})
	}

	return results, nil
}

// DiffPair is a pair of nodes that were changed in some way.
type DiffPair struct {
	Src StatInfo
	Dst StatInfo
}

// Diff gives a detailed overview over the changes between two commits.
type Diff struct {
	Added   []StatInfo
	Removed []StatInfo
	Ignored []StatInfo
	Missing []StatInfo

	Moved    []DiffPair
	Merged   []DiffPair
	Conflict []DiffPair
}

// IsEmpty reports if a diff is completely empty (i.e. nothing changed)
func (df *Diff) IsEmpty() bool {
	return 0 == 0+
		len(df.Added)+
		len(df.Removed)+
		len(df.Ignored)+
		len(df.Missing)+
		len(df.Moved)+
		len(df.Merged)+
		len(df.Conflict)
}

func convertDiffList(lst capnp.StatInfo_List) ([]StatInfo, error) {
	infos := []StatInfo{}

	for idx := 0; idx < lst.Len(); idx++ {
		capInfo := lst.At(idx)
		info, err := convertCapStatInfo(&capInfo)
		if err != nil {
			return nil, err
		}

		infos = append(infos, *info)
	}

	return infos, nil
}

func convertDiffPairList(lst capnp.DiffPair_List) ([]DiffPair, error) {
	pairs := []DiffPair{}
	for idx := 0; idx < lst.Len(); idx++ {
		capPair := lst.At(idx)
		capSrc, err := capPair.Src()
		if err != nil {
			return nil, err
		}

		capDst, err := capPair.Dst()
		if err != nil {
			return nil, err
		}

		srcInfo, err := convertCapStatInfo(&capSrc)
		if err != nil {
			return nil, err
		}

		dstInfo, err := convertCapStatInfo(&capDst)
		if err != nil {
			return nil, err
		}

		pairs = append(pairs, DiffPair{
			Src: *srcInfo,
			Dst: *dstInfo,
		})
	}

	return pairs, nil
}

func convertCapDiffToDiff(capDiff capnp.Diff) (*Diff, error) {
	diff := &Diff{}

	lst, err := capDiff.Added()
	if err != nil {
		return nil, err
	}

	diff.Added, err = convertDiffList(lst)
	if err != nil {
		return nil, err
	}

	lst, err = capDiff.Missing()
	if err != nil {
		return nil, err
	}

	diff.Missing, err = convertDiffList(lst)
	if err != nil {
		return nil, err
	}

	lst, err = capDiff.Removed()
	if err != nil {
		return nil, err
	}

	diff.Removed, err = convertDiffList(lst)
	if err != nil {
		return nil, err
	}

	lst, err = capDiff.Ignored()
	if err != nil {
		return nil, err
	}

	diff.Ignored, err = convertDiffList(lst)
	if err != nil {
		return nil, err
	}

	pairs, err := capDiff.Moved()
	if err != nil {
		return nil, err
	}

	diff.Moved, err = convertDiffPairList(pairs)
	if err != nil {
		return nil, err
	}

	pairs, err = capDiff.Merged()
	if err != nil {
		return nil, err
	}

	diff.Merged, err = convertDiffPairList(pairs)
	if err != nil {
		return nil, err
	}

	pairs, err = capDiff.Conflict()
	if err != nil {
		return nil, err
	}

	diff.Conflict, err = convertDiffPairList(pairs)
	if err != nil {
		return nil, err
	}

	return diff, nil
}

// MakeDiff creates a diff between the commits at `remoteRev` and `localRev`.
// If `needFetch` is true, the data is first updated from the remote.
func (ctl *Client) MakeDiff(local, remote, localRev, remoteRev string, needFetch bool) (*Diff, error) {
	call := ctl.api.MakeDiff(ctl.ctx, func(p capnp.VCS_makeDiff_Params) error {
		if err := p.SetLocalOwner(local); err != nil {
			return err
		}

		if err := p.SetRemoteOwner(remote); err != nil {
			return err
		}

		if err := p.SetLocalRev(localRev); err != nil {
			return err
		}

		p.SetNeedFetch(needFetch)
		return p.SetRemoteRev(remoteRev)
	})

	result, err := call.Struct()
	if err != nil {
		return nil, err
	}

	capDiff, err := result.Diff()
	if err != nil {
		return nil, err
	}

	return convertCapDiffToDiff(capDiff)
}

// Fetch updates our internal copy of the data of `remote`.
func (ctl *Client) Fetch(remote string) error {
	call := ctl.api.Fetch(ctl.ctx, func(p capnp.VCS_fetch_Params) error {
		return p.SetWho(remote)
	})

	_, err := call.Struct()
	return err
}

// Sync triggers a sync with the data from `remote`.
// If `needFetch` is true, the data is first updated from the remote.
func (ctl *Client) Sync(remote string, needFetch bool) (*Diff, error) {
	call := ctl.api.Sync(ctl.ctx, func(p capnp.VCS_sync_Params) error {
		p.SetNeedFetch(needFetch)
		return p.SetWithWhom(remote)
	})

	result, err := call.Struct()
	if err != nil {
		return nil, err
	}

	capDiff, err := result.Diff()
	if err != nil {
		return nil, err
	}

	return convertCapDiffToDiff(capDiff)
}

// CommitInfo is like a stat(2) for commits.
func (ctl *Client) CommitInfo(rev string) (bool, *Commit, error) {
	call := ctl.api.CommitInfo(ctl.ctx, func(p capnp.VCS_commitInfo_Params) error {
		return p.SetRev(rev)
	})

	result, err := call.Struct()
	if err != nil {
		return false, nil, err
	}

	if !result.IsValidRef() {
		return false, nil, nil
	}

	capCmt, err := result.Commit()
	if err != nil {
		return false, nil, err
	}

	cmt, err := convertCapCommit(&capCmt)
	if err != nil {
		return false, nil, err
	}

	return true, cmt, nil
}
